-/*
+/*
Support for Google Earth & Keyhole "kml" format.
- Copyright (C) 2005, 2006, 2007 Robert Lipe, robertlipe@usa.net
+ Copyright (C) 2005, 2006, 2007, 2008, 2009 Robert Lipe,
+ robertlipe@gpsbabel.org
Updates by Andrew Kirmse, akirmse at google.com
This program is free software; you can redistribute it and/or modify
static point3d *point3d_list;
static int realtime_positioning;
static int do_indentation = 1;
+static bounds kml_bounds;
+static time_t kml_time_min;
+static time_t kml_time_max;
#define TD(FMT,DATA) kml_write_xml(0, "<tr><td>" FMT " </td></tr>\n", DATA)
#define TD2(FMT,DATA, DATA2) kml_write_xml(0, "<tr><td>" FMT " </td></tr>\n", DATA, DATA2)
// Icons provided and hosted by Google. Used with permission.
#define ICON_BASE "http://earth.google.com/images/kml-icons/"
-static const char kml22_hdr[] =
- "<kml xmlns=\"http://earth.google.com/kml/2.2\"\n"
- "\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
-// No "baked in" schemaLocation, per Google recommendation.
-// "\txsi:schemaLocation=\"http://earth.google.com/kml/2.2 \n"
-// "\thttp://code.google.com/apis/kml/schema/kml22beta.xsd\">\n";
-
-static const char kml21_hdr[] =
- "<kml xmlns=\"http://earth.google.com/kml/2.1\"\n"
- "\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n";
-// No "baked in" schemaLocation, per Google recommendation.
-// "\txsi:schemaLocation=\"http://earth.google.com/kml/2.1 \n"
-// "\thttp://code.google.com/apis/kml/schema/kml21.xsd\">\n";
+static const char kml22_hdr[] =
+ "<kml xmlns=\"http://www.opengis.net/kml/2.2\"\n"
+ "\txmlns:gx=\"http://www.google.com/kml/ext/2.2\">\n";
+
static
arglist_t kml_args[] = {
{"deficon", &opt_deficon, "Default icon name", NULL, ARGTYPE_STRING, ARG_NOMINMAX },
- {"lines", &opt_export_lines,
+ {"lines", &opt_export_lines,
"Export linestrings for tracks and routes",
"1", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"points", &opt_export_points,
+ {"points", &opt_export_points,
"Export placemarks for tracks and routes",
"1", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"line_width", &opt_line_width,
+ {"line_width", &opt_line_width,
"Width of lines, in pixels",
"6", ARGTYPE_INT, ARG_NOMINMAX },
- {"line_color", &opt_line_color,
+ {"line_color", &opt_line_color,
"Line color, specified in hex AABBGGRR",
"99ffac59", ARGTYPE_STRING, ARG_NOMINMAX },
- {"floating", &opt_floating,
- "Altitudes are absolute and not clamped to ground",
+ {"floating", &opt_floating,
+ "Altitudes are absolute and not clamped to ground",
"0", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"extrude", &opt_extrude,
- "Draw extrusion line from trackpoint to ground",
+ {"extrude", &opt_extrude,
+ "Draw extrusion line from trackpoint to ground",
"0", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"trackdata", &opt_trackdata,
- "Include extended data for trackpoints (default = 1)",
+ {"trackdata", &opt_trackdata,
+ "Include extended data for trackpoints (default = 1)",
"1", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"trackdirection", &opt_trackdirection,
- "Indicate direction of travel in track icons (default = 0)",
+ {"trackdirection", &opt_trackdirection,
+ "Indicate direction of travel in track icons (default = 0)",
"0", ARGTYPE_BOOL, ARG_NOMINMAX },
- {"units", &opt_units,
- "Units used when writing comments ('s'tatute or 'm'etric)",
+ {"units", &opt_units,
+ "Units used when writing comments ('s'tatute or 'm'etric)",
"s", ARGTYPE_STRING, ARG_NOMINMAX },
{"labels", &opt_labels,
"Display labels on track and routepoints (default = 1)",
ARG_TERMINATOR
};
-static
+static
struct {
int freshness;
const char *icon;
{ 0, ICON_BASE "youarehere-0.png" }, // Green
};
-#define ICON_NOSAT ICON_BASE "youarehere-warning.png";
+#define ICON_NOSAT ICON_BASE "youarehere-warning.png";
#define ICON_WPT "http://maps.google.com/mapfiles/kml/pal4/icon61.png"
#define ICON_TRK ICON_BASE "track-directional/track-none.png"
-#define ICON_RTE ICON_BASE "track-directional/track-none.png"
+#define ICON_RTE ICON_BASE "track-directional/track-none.png"
#define ICON_DIR ICON_BASE "track-directional/track-%d.png" // format string where next arg is rotational degrees.
#define MYNAME "kml"
static xg_callback wpt_s, wpt_e;
static xg_callback wpt_name, wpt_desc, wpt_coord, wpt_icon, trk_coord, wpt_time;
-static
+static
xg_tag_mapping kml_map[] = {
{ wpt_s, cb_start, "/Placemark" },
{ wpt_e, cb_end, "/Placemark" },
{ NULL, 0, NULL }
};
-static
+static
const char * kml_tags_to_ignore[] = {
"kml",
"Document",
NULL,
};
-void wpt_s(const char *args, const char **unused)
-{
+void wpt_s(const char *args, const char **unused)
+{
wpt_tmp = waypt_new();
wpt_tmp_queued = 0;
}
}
}
-static
+static
void
kml_rd_init(const char *fname)
{
kml_wr_init(const char *fname)
{
char u = 's';
+ waypt_init_bounds(&kml_bounds);
+ kml_time_min = 0;
+ kml_time_max = 0;
if (opt_units) {
u = tolower(opt_units[0]);
- }
+ }
switch(u) {
case 's': fmt_setunits(units_statute); break;
ofd = gbfopen(fname, "w", MYNAME);
}
-/*
+/*
* The magic here is to try to ensure that posnfilename is atomically
* updated.
*/
}
#define hovertag(h) h ? 'h' : 'n'
-static void kml_write_bitmap_style_(const char *style, const char * bitmap,
+static void kml_write_bitmap_style_(const char *style, const char * bitmap,
int highlighted, int force_heading)
{
- kml_write_xml(0, "<!-- %s %s style -->\n",
+ kml_write_xml(0, "<!-- %s %s style -->\n",
highlighted ? "Highlighted" : "Normal", style);
kml_write_xml(1, "<Style id=\"%s_%c\">\n", style, hovertag(highlighted));
kml_write_xml(1, "<IconStyle>\n");
kml_write_xml(-1, "</Icon>\n");
kml_write_xml(-1, "</IconStyle>\n");
kml_write_xml(-1, "</Style>\n");
-}
+}
/* A wrapper for the above function to emit both a highlighted
* and non-highlighted version of the style to allow the icons
if (waypointp->creation_time) {
xml_fill_in_time(time_string, waypointp->creation_time, waypointp->microseconds, XML_LONG_TIME);
if (time_string[0]) {
- kml_write_xml(0, "<TimeStamp><when>%s</when></TimeStamp>\n",
+ kml_write_xml(0, "<TimeStamp><when>%s</when></TimeStamp>\n",
time_string);
}
}
/*
* Output the track summary.
*/
-static
+static
void kml_output_trkdescription(const route_head *header, computed_trkdata *td)
{
char *max_alt_units;
}
-static
+static
void kml_output_header(const route_head *header, computed_trkdata*td)
{
if (!realtime_positioning) {
TD2("Speed: %.1f %s", spd, spd_units);
}
if WAYPT_HAS(pt, course) TD("Heading: %.1f", pt->course);
- /* This really shouldn't be here, but as of this writing,
- * Earth can't edit/display the TimeStamp.
+ /* This really shouldn't be here, but as of this writing,
+ * Earth can't edit/display the TimeStamp.
*/
if (pt->creation_time) {
char time_string[64];
- xml_fill_in_time(time_string, pt->creation_time,
+ xml_fill_in_time(time_string, pt->creation_time,
pt->microseconds, XML_LONG_TIME);
if (time_string[0]) {
TD("Time: %s", time_string);
kml_write_xml(-1, "]]></description>\n");
}
+static void kml_recompute_time_bounds(const waypoint *waypointp) {
+ if (waypointp->creation_time && (waypointp->creation_time < kml_time_min)) {
+ kml_time_min = waypointp->creation_time;
+ }
+ if (waypointp->creation_time > kml_time_max) {
+ kml_time_max = waypointp->creation_time;
+ if (kml_time_min == 0) {
+ kml_time_min = waypointp->creation_time;
+ }
+ }
+}
+
static void kml_output_point(const waypoint *waypointp, kml_point_type pt_type)
{
const char *style;
point3d *pt = &point3d_list[point3d_list_len];
point3d_list_len++;
+ waypt_add_to_bounds(&kml_bounds, waypointp);
+ kml_recompute_time_bounds(waypointp);
+
switch (pt_type) {
case kmlpt_track: style = "#track"; break;
case kmlpt_route: style = "#route"; break;
} else {
if (trackdirection && (pt_type == kmlpt_track)) {
char buf[100];
- if (waypointp->speed < 1)
+ if (waypointp->speed < 1)
snprintf(buf, sizeof(buf), "%s-none", style);
else
- snprintf(buf, sizeof(buf), "%s-%d", style,
+ snprintf(buf, sizeof(buf), "%s-%d", style,
(int) (waypointp->course / 22.5 + .5) % 16);
kml_write_xml(0, "<styleUrl>%s</styleUrl>\n", buf);
} else {
if (export_points && point3d_list_len > 0) {
kml_write_xml(-1, "</Folder>\n");
}
-
+
// Add a linestring for this track?
if (export_lines && point3d_list_len > 0) {
kml_write_xml(1, "<Placemark>\n");
kml_write_xml(0, "<tessellate>1</tessellate>\n");
kml_write_xml(1, "<coordinates>\n");
for (i = 0; i < point3d_list_len; ++i)
- kml_write_xml(0, "%f,%f,%f\n",
+ kml_write_xml(0, "%f,%f,%f\n",
point3d_list[i].longitude,
point3d_list[i].latitude,
point3d_list[i].altitude);
-
+
kml_write_xml(-1, "</coordinates>\n");
kml_write_xml(-1, "</LineString>\n");
kml_write_xml(-1, "</Placemark>\n");
}
-
+
xfree(point3d_list);
point3d_list = NULL;
-
+
if (!realtime_positioning) {
kml_write_xml(-1, "</Folder>\n");
}
/*
* Completely different writer for geocaches.
*/
-static
-void kml_gc_make_ballonstyle(void)
+static
+void kml_gc_make_ballonstyle(void)
{
// BalloonStyle for Geocaches.
kml_write_xml(1, "<Style id=\"geocache\"><BalloonStyle><text><![CDATA[\n");
kml_write_xml(-1, "]]></text></BalloonStyle></Style>\n");
}
-static
+static
char *
kml_lookup_gc_icon(const waypoint *waypointp)
{
char *p, *is;
double lat = waypointp->latitude;
double lng = waypointp->longitude;
-// optionally "fuzz" lat/lng here.
kml_write_xml(1, "<Placemark>\n");
kml_write_xml(0, "<Data name=\"gc_cont_icon\"><value>%s</value></Data>\n", kml_lookup_gc_container(waypointp));
- // Highlight any issues with the cache, such as temp unavail
+ // Highlight any issues with the cache, such as temp unavail
// or archived.
kml_write_xml(0, "<Data name=\"gc_issues\"><value>");
if (waypointp->gc_data->is_archived == status_true) {
kml_write_xml(-1, "</LookAt>\n");
}
#endif
+ waypt_add_to_bounds(&kml_bounds, waypointp);
+ kml_recompute_time_bounds(waypointp);
if (waypointp->gc_data->diff && waypointp->gc_data->terr) {
kml_geocache_pr(waypointp);
}
kml_write_xml(0, "<coordinates>%f,%f,%f</coordinates>\n",
- waypointp->longitude, waypointp->latitude,
+ waypointp->longitude, waypointp->latitude,
waypointp->altitude == unknown_alt ? 0.0 : waypointp->altitude);
kml_write_xml(-1, "</Point>\n");
* TRACKPOINTS
*/
-static void kml_track_hdr(const route_head *header)
+static void kml_track_hdr(const route_head *header)
{
computed_trkdata *td;
track_recompute(header, &td);
kml_output_point(waypointp, kmlpt_track);
}
-static void kml_track_tlr(const route_head *header)
+static void kml_track_tlr(const route_head *header)
{
kml_output_tailer(header);
}
* ROUTES
*/
-static void kml_route_hdr(const route_head *header)
+static void kml_route_hdr(const route_head *header)
{
kml_output_header(header, NULL);
}
kml_output_point(waypointp, kmlpt_route);
}
-static void kml_route_tlr(const route_head *header)
+static void kml_route_tlr(const route_head *header)
{
kml_output_tailer(header);
}
+// For Earth 5.0, we write a LookAt that encompasses
+// the bounding box of our entire data set and set the event times
+// to include all our data.
+void kml_write_AbstractView(void) {
+ kml_write_xml(1, "<LookAt>\n");
+
+ if (kml_time_min || kml_time_max) {
+ kml_write_xml(1, "<gx:TimeSpan>\n");
+ if (kml_time_min) {
+ char time_string[64];
+ xml_fill_in_time(time_string, kml_time_min, 0, XML_LONG_TIME);
+ if (time_string[0]) {
+ kml_write_xml(0, "<begin>%s</begin>\n", time_string);
+ }
+ }
+ if (kml_time_max) {
+ char time_string[64];
+ xml_fill_in_time(time_string, kml_time_max, 0, XML_LONG_TIME);
+ if (time_string[0]) {
+ kml_write_xml(0, "<end>%s</end>\n", time_string);
+ }
+ }
+ kml_write_xml(-1, "</gx:TimeSpan>\n");
+ }
+
+ // If our BB spans the antemeridian, flip sign on one.
+ // This doesn't make our BB optimal, but it at least prevents us from
+ // zooming to the wrong hemisphere.
+ if (kml_bounds.min_lon * kml_bounds.max_lon < 0) {
+ kml_bounds.min_lon = -kml_bounds.max_lon;
+ }
+
+ kml_write_xml(0, "<longitude>%f</longitude>\n",
+ (kml_bounds.min_lon + kml_bounds.max_lon) / 2);
+ kml_write_xml(0, "<latitude>%f</latitude>\n",
+ (kml_bounds.min_lat + kml_bounds.max_lat) / 2);
+
+ // It turns out the length of the diagonal of the bounding box gives us a
+ // reasonable guess for setting the camera altitude.
+ double bb_size = gcgeodist(kml_bounds.min_lat, kml_bounds.min_lon,
+ kml_bounds.max_lat, kml_bounds.max_lon);
+ // Clamp bottom zoom level. Otherwise, a single point zooms to grass.
+ if (bb_size < 1000) {
+ bb_size = 1000;
+ }
+ kml_write_xml(0, "<range>%f</range>\n", bb_size * 1.3);
+
+ kml_write_xml(-1, "</LookAt>\n");
+}
+
void kml_write(void)
{
char import_time[100];
kml_write_xml(0, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
- /*
- * This is a bit cowardly. Our geocache writer takes advantage
- * of KML 2.2 features present only in Earth 4.2 and newer. The
- * output is actually compatible with Earth versions back to 4.0
- * for the non-geocaching case, so we'll be conservative and not
- * output the new namespace if we don't need it.
- */
- if (geocaches_present) {
- kml_write_xml(1, kml22_hdr);
- } else {
- kml_write_xml(1, kml21_hdr);
- }
+ kml_write_xml(1, kml22_hdr);
kml_write_xml(1, "<Document>\n");
now = current_time();
strftime(import_time, sizeof(import_time), "%c", localtime(&now));
- if (realtime_positioning)
+ if (realtime_positioning)
kml_write_xml(0, "<name>GPS position</name>\n");
else
kml_write_xml(0, "<name>GPS device</name>\n");
}
kml_write_bitmap_style(kmlpt_waypoint, ICON_WPT, NULL);
-
+
if (track_waypt_count() || route_waypt_count()) {
// Style settings for line strings
kml_write_xml(1, "<Style id=\"lineStyle\">\n");
kml_write_xml(1, "<Folder>\n");
kml_write_xml(0, "<name>Routes</name>\n");
- route_disp_all(kml_route_hdr,
+ route_disp_all(kml_route_hdr,
kml_route_tlr, kml_route_disp);
kml_write_xml(-1, "</Folder>\n");
}
}
+ kml_write_AbstractView();
kml_write_xml(-1, "</Document>\n");
kml_write_xml(-1, "</kml>\n");
}
{
waypoint *newest_posn= (waypoint *) QUEUE_LAST(&posn_trk_head->waypoint_list);
- if(radtometers(gcdist(RAD(wpt->latitude), RAD(wpt->longitude),
+ if(radtometers(gcdist(RAD(wpt->latitude), RAD(wpt->longitude),
RAD(newest_posn->latitude), RAD(newest_posn->longitude))) > 50) {
track_add_wpt(posn_trk_head, waypt_dupe(wpt));
} else {
- /* If we haven't move more than our threshold, pretend
- * we didn't move at all to prevent Earth from jittering
+ /* If we haven't move more than our threshold, pretend
+ * we didn't move at all to prevent Earth from jittering
* the zoom levels on us.
*/
wpt->latitude = newest_posn->latitude;
* If we are keeping only a recent subset of the trail, trim the
* head here.
*/
- while (max_position_points &&
+ while (max_position_points &&
(posn_trk_head->rte_waypt_ct >= max_position_points)) {
waypoint *tonuke = (waypoint *) QUEUE_FIRST(&posn_trk_head->waypoint_list);
track_del_wpt(posn_trk_head, tonuke);
kml_wr_deinit,
kml_read,
kml_write,
- NULL,
+ NULL,
kml_args,
CET_CHARSET_UTF8, 1, /* CET-REVIEW */
{ NULL, NULL, NULL, kml_wr_position_init, kml_wr_position, kml_wr_position_deinit }